package ws.jdbc.concurrent.eresultset;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.locks.ReentrantLock;
import ws.jdbc.concurrent._development.DevMode;
import ws.jdbc.concurrent.exception.JdbcConcurrentException;

/**
 * This class provides cursors access to a {@link ResultSet} object and manages
 * the change of cursor working on that result set.
 * 
 * @author Wilson Santos, 36228 <wilson.santos@ua.pt>
 */
public class EResultSet implements ICursorFactory {

  /** ResultSet object for the cursors work upon. */
  private ResultSet rs;
  /** Explicit lock when {@link #lock()} is called. */
  private final ReentrantLock lock = new ReentrantLock();
  /** Tells if the object is explicitly locked. */
  private boolean islocked = false;
  /** Number of created cursors. */
  private int cursorCount = 0;
  /** ID of the cursor that is currently using the ResultSet. */
  private int currentCursor = 0;
  /** Maximum number of cursors that can be created. */
  private final int maxCursorCount;
  /**
   * Current result set row. 
   * This value is used to avoid having to call getRow(), thus improving performance. 
   */
  private int currentRow = 0;
  /** Array with the save row number for each cursor. {@code context[0]} = row for cursor #0, etc. */
  protected int[] context = new int[500];
  /** Cursor if after the last element of the result set. */
  protected static final int AFTER_LAST = Integer.MAX_VALUE;
  /** Cursor if before the first element of the result set. */
  protected static final int BEFORE_FIRST = Integer.MIN_VALUE;

  /**
   * Create a new instance.
   *
   * @param rs ResultSet object for the cursors work upon.
   */
  public EResultSet(ResultSet rs) {
    this.rs = rs;
    maxCursorCount = 500;
    context = new int[maxCursorCount];
    resetContext();
  }

  /**
   * Create a new instance.
   * 
   * @param rs ResultSet object for the cursors work upon.
   * @param numberOfCursors number of cursors that this object will be able to create,
   */
  public EResultSet(ResultSet rs, int numberOfCursors) {
    this.rs = rs;
    maxCursorCount = numberOfCursors;
    context = new int[maxCursorCount];
    resetContext();
  }

  /**
   * Create a new cursor.
   *
   * @return cursor.
   *
   * @throws JdbcConcurrentException if cannot create more cursors because limit has been reached.
   */
  public ICursor createCursor() throws JdbcConcurrentException {
    if (++cursorCount > maxCursorCount) {
      throw new JdbcConcurrentException(JdbcConcurrentException.ERESULTSET_TOO_MUCH_CURSORS);
    }
    return new Cursor(this, cursorCount - 1);
  }

  protected int getRow(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    currentCursor = cursorId;

    final int ret = context[cursorId];

    lock.unlock();
    return ret;
  }

  protected boolean next(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    currentCursor = cursorId;

    final boolean ret = rs.next();
    if (ret) {
      currentRow++;
    } else {
      currentRow = AFTER_LAST;
    }
    saveContext();

    lock.unlock();

    return ret;
  }

  protected boolean absolute(int cursorId, int row) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, "moving to " + row);
    }

    final boolean ret = rs.absolute(row);
    currentCursor = cursorId;
    if (ret) {
      currentRow = row;
    } else if (row > 0) {
      currentRow = AFTER_LAST;
    } else if (row <= 0) {
      currentRow = BEFORE_FIRST;
    }
    saveContext();

    lock.unlock();

    return ret;
  }

  protected void afterLast(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    rs.afterLast();
    currentRow = AFTER_LAST;
    currentCursor = cursorId;
    saveContext();

    lock.unlock();
  }

  protected void beforeFirst(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    currentRow = BEFORE_FIRST;
    currentCursor = cursorId;
    saveContext();

    lock.unlock();
  }

  protected boolean previous(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    currentCursor = cursorId;
    final boolean ret = rs.previous();
    if (ret) {
      currentRow--;
    } else {
      currentRow = BEFORE_FIRST;
    }
    saveContext();

    lock.unlock();

    return ret;
  }

  protected boolean relative(int cursorId, int rows) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    currentCursor = cursorId;
    final boolean ret = rs.relative(rows);
    if (ret) {
      currentRow += rows;
    } else if (rows > 0) {
      currentRow = AFTER_LAST;
    } else {
      currentRow = BEFORE_FIRST;
    }
    saveContext();

    lock.unlock();

    return ret;
  }

  protected int getInt(int cursorId, int columnIndex) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    final int ret;
    try {
      restoreContext(cursorId);
      currentCursor = cursorId;
      ret = rs.getInt(columnIndex);
    } finally {
      lock.unlock();
    }
    return ret;
  }

  protected String getString(int cursorId, int columnIndex) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    final String ret;
    try {
      restoreContext(cursorId);
      currentCursor = cursorId;
      ret = rs.getString(columnIndex);
    } finally {
      lock.unlock();
    }
    return ret;
  }

  protected float getFloat(int cursorId, int columnIndex) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    final float ret;
    try {
      restoreContext(cursorId);
      currentCursor = cursorId;
      ret = rs.getFloat(columnIndex);
    } finally {
      lock.unlock();
    }
    return ret;
  }

  protected void moveToInsertRow(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    currentCursor = cursorId;
    rs.moveToInsertRow();
  }

  protected void moveToCurrentRow(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    try {
      /* There's no need to call rs.moveToCurrentRow because restoreContext moves
       * the cursor to the desired row.
       */
      restoreContext(cursorId);
      currentCursor = cursorId;
    } finally {
      unlockUpdate();
    }
  }

  protected void cancelRowUpdates(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    try {
      currentCursor = cursorId;
      rs.cancelRowUpdates();
    } finally {
      unlockUpdate();
    }
  }

  protected void updateFloat(int cursorId, int columnIndex, float x) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    currentCursor = cursorId;
    rs.updateFloat(columnIndex, x);
  }

  protected void updateInt(int cursorId, int columnIndex, int x) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId); // won't do nothing if cursorId == currentCursor, therefore the current row updates will not be lost.
    currentCursor = cursorId;
    rs.updateInt(columnIndex, x);
  }

  protected void updateString(int cursorId, int columnIndex, String x) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    currentCursor = cursorId;
    rs.updateString(columnIndex, x);
  }

  protected void updateRow(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    currentCursor = cursorId;
    rs.updateRow();
    unlockUpdate();
  }

  protected void insertRow(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    currentCursor = cursorId;
    rs.insertRow();
    unlockUpdate();
  }

  protected boolean isBeforeFirst(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    final boolean ret = (currentRow == BEFORE_FIRST);

    lock.unlock();

    return ret;
  }

  protected boolean isAfterLast(int cursorId) throws SQLException {
    lock.lock();

    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    restoreContext(cursorId);
    final boolean ret = (currentRow == AFTER_LAST);

    lock.unlock();

    return ret;
  }

  /**
   * Clear the array that saves the cursor context.
   */
  private void resetContext() {
    for (int i = 0; i < context.length; ++i) {
      context[i] = 0;
    }
  }

  /**
   * Restore the cursor context.
   * It reads the row from memory and moves the result set into it.
   * It also saves the context of the previous cursor.
   *
   * @param cursorId id of the cursor whose context will be restored.
   * @throws SQLException
   */
  private synchronized void restoreContext(int cursorId) throws SQLException {
    if (DevMode.concurrent_eresultset) {
      DevMode.printlog(cursorId, null);
    }

    if (currentCursor != cursorId) {
      currentRow = context[cursorId];
      rs.absolute(currentRow);

      if (DevMode.concurrent_eresultset) {
        DevMode.printlog(cursorId, "retrieved context.");
      }
    }
  }

  /**
   * Save the cursor context. It saves the current result set row.
   *
   * @param cursorId id of the cursor whose context will be saved.
   *
   * @throws SQLException if an database error occurs while getting the current row.
   */
  private synchronized void saveContext() throws SQLException {
    context[currentCursor] = currentRow;
  }

  public final void lock() {
    lock.lock();
    islocked = true;
  }

  public synchronized final void unlock() {
    for (int i = 0; i < lock.getHoldCount(); ++i) {
      lock.unlock();
    }
  }

  /**
   * Release all locks if explicit lock is {@code false}, otherwise release all
   * lock except for the explicit one.
   */
  protected synchronized final void unlockUpdate() {
    int lockCount = islocked ? lock.getHoldCount() - 1 : lock.getHoldCount();
    for (int i = 0; i < lockCount; ++i) {
      lock.unlock();
    }
  }
}
